package net.intellect.force;

import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.log4j.Logger;

import com.sforce.soap.enterprise.DeleteResult;
import com.sforce.soap.enterprise.ID;
import com.sforce.soap.enterprise.InvalidFieldFault;
import com.sforce.soap.enterprise.InvalidQueryLocatorFault;
import com.sforce.soap.enterprise.LoginResult;
import com.sforce.soap.enterprise.MalformedQueryFault;
import com.sforce.soap.enterprise.QueryLocator;
import com.sforce.soap.enterprise.QueryOptions;
import com.sforce.soap.enterprise.QueryResult;
import com.sforce.soap.enterprise.SaveResult;
import com.sforce.soap.enterprise.SessionHeader;
import com.sforce.soap.enterprise.SforceServiceStub;
import com.sforce.soap.enterprise.InvalidIdFault;
import com.sforce.soap.enterprise.InvalidSObjectFault;
import com.sforce.soap.enterprise.LoginFault;
import com.sforce.soap.enterprise.UnexpectedErrorFault;
import com.sforce.soap.enterprise.sobject.SObject;

public class ClientSalesforce {
	
	protected static Logger logger = Logger.getLogger("salesforce");
	
	public static final String HOST_PRODUCTION = "https://www.salesforce.com/services/Soap/c/17.0";
	public static final String HOST_DEVELOPER = "https://test.salesforce.com/services/Soap/c/17.0";
	public static final int MAXIMO_IDS_IN = 300;
	
	// Private wrapper class variables
	private String username;
	private String password;
	private ID 	   userID;
	private String token;
	private String host;
	private int    querySize;
	private int    sessionlength;
	private String sessionId;
	private String serverUrl;
	private long   nextLoginTime;
	private long   timeOut = 60000;
	
	private LoginResult 		loginResult;
	private SessionHeader 		sh;
	private SforceServiceStub 	binding;
	private boolean 			useCompression;
	private QueryOptions 		queryOptions;	
	
	
	/*** This method initializes the private class variables
	*/
	public ClientSalesforce() {
		this(false);
	}
	
	
	public ClientSalesforce(Boolean isSandbox) {
		this.querySize = 500;
		this.sessionlength = 29;
		this.useCompression = true;
		if (isSandbox) {			
			this.host = HOST_DEVELOPER;
		} else {
			this.host = HOST_PRODUCTION;
		}
	}
	
	public ClientSalesforce(String user,String password,String token,String urlProxy,String puertoProxy) {
		this(false,user,password,token,urlProxy,puertoProxy);
	}			

	public ClientSalesforce(Boolean isSandbox,String user,String password,String token,String urlProxy,String puertoProxy) {
		if (urlProxy!=null && urlProxy.length()>0) {
			System.setProperty("http.proxyHost", urlProxy);
			System.setProperty("http.proxyPort", puertoProxy);
		}
				
		
		this.querySize = 500;
		this.sessionlength = 29;
		this.useCompression = true;
		this.host = HOST_PRODUCTION;
		this.username = user;
		this.password = password;
		this.token = token;
		if (isSandbox) {			
			this.host = HOST_DEVELOPER;
		} else {
			this.host = HOST_PRODUCTION;
		}
		
	}			
	
	
	/**
	* These methods get and set the private class variables
	*/
	public String getUsername() {
		return this.username;
	}
	public void setUsername(String value) {
		this.username = value;
	}
	
	public String getPassword() {
		return this.password;
	}
	public void setPassword(String value) {
		this.password = value;
	}
	
	public String getToken() {
		return this.token;
	}
	public void setToken(String value) {
		this.token = value;
	}
	
	public String getHost() {
		return this.host;
	}
	public void setHost(String value) {
		this.host = value;
	}

	
	public String getServerURL() {
		return this.serverUrl;
	}
	
	public int getQuerySize() {
		return this.querySize;
	}
	public void setQuerySize(int value) {
		this.querySize = value;
	}
	
	public int getSessionlength() {
		return this.sessionlength;
	}
	public void setSessionlength(int value) {
		this.sessionlength = value;
	}
	
	public boolean getUseCompression() {
		return this.useCompression;
	}
	public void setUseCompression(boolean value) {
		this.useCompression = value;
		setCompressionOnBinding();
	}
	
	/**
	* This method is used to log in to salesforce and set the
	* private class variables for the wrapper, including the
	* session ID.
	*/
	public void login() throws UnexpectedErrorFault, InvalidIdFault,LoginFault, RemoteException, Exception {
		resetBindingStub();
		this.binding._getServiceClient().getOptions().setTo(new org.apache.axis2.addressing.EndpointReference(host));		
		
		loginResult = binding.login(username,token!=null?password+token:password,null);
		this.nextLoginTime = System.currentTimeMillis() +(this.sessionlength * timeOut);
				
		this.sessionId = loginResult.getSessionId();
		this.serverUrl = loginResult.getServerUrl();
		
		//Create a new session header object and set the
		//session id to that returned by the login
		sh = new SessionHeader();
		sh.setSessionId(loginResult.getSessionId());		
				
		binding = new SforceServiceStub(loginResult.getServerUrl());
		setCompressionOnBinding();
		this.userID = loginResult.getUserId();
		
	}
	
	/**
	 * Este metodo verifica si es necesario loguearse en salesforce, y
	 * en caso que lo sea realiza el login
	 * @throws UnexpectedErrorFault
	 * @throws InvalidIdFault
	 * @throws LoginFault
	 * @throws RemoteException
	 * @throws Exception
	 */
	private void checkLogin() throws UnexpectedErrorFault,InvalidIdFault, LoginFault, RemoteException, Exception {
		if (this.loginRequired())
			login();
	}
	
	/**
	* This method is used to log in with an existing sessionId
	* @param String sid sessionId* @param String sURL serverUrl
	*/
	public void loginBySessionId(String sid, String sURL) throws Exception {
		this.nextLoginTime = System.currentTimeMillis() +(this.sessionlength * timeOut);
		resetBindingStub();
		this.binding._getServiceClient().getOptions().setTo(new org.apache.axis2.addressing.EndpointReference(""));		
		this.sessionId = sid;
		this.serverUrl = sURL;
		SessionHeader sh = new SessionHeader();
		sh.setSessionId(sid);
		binding = new SforceServiceStub(loginResult.getServerUrl());
		setCompressionOnBinding();		
	}
	
	/** This method resets the binding object back to its
	* initial state.
	*/
	private void resetBindingStub() throws Exception {
		sh = new SessionHeader();
		loginResult = new LoginResult();		
		this.binding = new SforceServiceStub();		
		this.binding._getServiceClient().getOptions().setTimeOutInMilliSeconds(timeOut);	
		setCompressionOnBinding();
		this.queryOptions = null;
		this.userID = null;
	}
	
	private void setCompressionOnBinding() {
		binding._getServiceClient().getOptions().setProperty(HTTPConstants.MC_ACCEPT_GZIP,useCompression);
		binding._getServiceClient().getOptions().setProperty(HTTPConstants.MC_GZIP_REQUEST,useCompression);
	}	
	
	
	/**
	* This method checks whether the session is active or not
	* @return boolean
	*/
	public boolean isConnected() {
		return System.currentTimeMillis() < nextLoginTime;
	}
	/**
	* This method returns true if a login to Salesforce is
	* necessary, otherwise false. It should be used to check the
	* session length before performing any API calls.
	*/
	private boolean loginRequired() {
		if (sessionId == null || sessionId.length() == 0)
			return true;
		return !isConnected();
	}
	
	private void setBatchSizeHeader(int batchSize) {
		if (queryOptions == null) {
			this.queryOptions = new QueryOptions();
		}
		queryOptions.setBatchSize(batchSize);
	}
	
	
	/**
	* This method queries the database and returns the results.
	* @param String strSOQLStmt
	* @return SObject[]
	*/
	public QueryResult executeQuery(String strSOQLStmt,Integer queryBatchSize) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		checkLogin();
		setBatchSizeHeader(queryBatchSize ==null ? querySize : queryBatchSize);					
		return binding.query(strSOQLStmt, sh, queryOptions, null, null);
	}
	
	public QueryResult executeQuery(String strSOQLStmt) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException, Exception {
		return executeQuery(strSOQLStmt, null);
	}
	
	public QueryResult executeQueryMore(QueryLocator queryLocator) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		checkLogin();
		return binding.queryMore(queryLocator,sh,queryOptions);
	}
	
	/**
	 * Return un SObject de Salesforce a partir de un sql
	 * @param strSOQLStmt
	 * @return
	 * @throws RemoteException
	 * @throws LoginFault
	 * @throws InvalidSObjectFault
	 * @throws MalformedQueryFault
	 * @throws InvalidIdFault
	 * @throws InvalidFieldFault
	 * @throws UnexpectedErrorFault
	 * @throws InvalidQueryLocatorFault
	 * @throws Exception
	 */
	public SObject getObjectSalesforce(String strSOQLStmt)throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {
		return getObjectSalesforce(strSOQLStmt, null);
	}
	
	
	public SObject getObjectSalesforce(String strSOQLStmt,Integer queryBatchSize)throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception { 	
		SObject object = null;
		logger.info("SQL = "  + strSOQLStmt);			
		QueryResult qr = this.executeQuery(strSOQLStmt,queryBatchSize);			
		logger.info("Total de registros encontrados: "  + qr.getSize());
		if (qr.getSize() > 0){
			object = (SObject)qr.getRecords()[0];
		}
		return object;				
	}
	
	
	/**
	 * Return un SObject de Salesforce a partir de un sql
	 * @param strSOQLStmt
	 * @return
	 * @throws RemoteException
	 * @throws LoginFault
	 * @throws InvalidSObjectFault
	 * @throws MalformedQueryFault
	 * @throws InvalidIdFault
	 * @throws InvalidFieldFault
	 * @throws UnexpectedErrorFault
	 * @throws InvalidQueryLocatorFault
	 * @throws Exception
	 */
	public int getCountSalesforce(String strSOQLStmt)throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {
		return getCountSalesforce(strSOQLStmt, null);
	}
	
	
	public int getCountSalesforce(String strSOQLStmt,Integer queryBatchSize)throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception { 	
		int count = 0;
		logger.info("SQL = "  + strSOQLStmt);			
		QueryResult qr = this.executeQuery(strSOQLStmt,queryBatchSize);			
		if (qr!=null) {			
			count = qr.getSize();
		}		
		logger.info("Count() = "  + count);
		return count;				
	}

	
	

	/**
	 * Return una lista de SObject de Salesforce a partir de un sql
	 * @param strSOQLStmt
	 * @param queryBatchSize
	 * @return
	 * @throws RemoteException
	 * @throws LoginFault
	 * @throws InvalidSObjectFault
	 * @throws MalformedQueryFault
	 * @throws InvalidIdFault
	 * @throws InvalidFieldFault
	 * @throws UnexpectedErrorFault
	 * @throws InvalidQueryLocatorFault
	 * @throws Exception
	 */	
	public List<SObject> getObjectsSalesforce(String strSOQLStmt,Integer queryBatchSize) throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {
		List<SObject> objects = new ArrayList<SObject>();
		logger.info("SQL = "  + strSOQLStmt);			
		QueryResult qr = this.executeQuery(strSOQLStmt,queryBatchSize);			
		logger.info("Total de registros encontrados: "  + qr.getSize());
		boolean done = false;
		if (qr.getSize() > 0){
			do {
				if (qr.getRecords()!=null) {
					for (int i=0;i<qr.getRecords().length;i++) {
						objects.add((SObject)qr.getRecords()[i]);
					}
				}
				done = qr.getDone();
				if (!done) {					
					qr = this.executeQueryMore(qr.getQueryLocator());
				}
			} while (!done);
		}
		return objects;
	}

	

	public List<SObject> getObjectsSalesforce(String strSOQLStmt) throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {
		return this.getObjectsSalesforce(strSOQLStmt,null);
	}
	
	/**
	 * Devuelve una lista de SObjects, a partir de un QueryResult
	 * @param qr
	 * @return
	 * @throws RemoteException
	 * @throws LoginFault
	 * @throws InvalidSObjectFault
	 * @throws MalformedQueryFault
	 * @throws InvalidIdFault
	 * @throws InvalidFieldFault
	 * @throws UnexpectedErrorFault
	 * @throws InvalidQueryLocatorFault
	 * @throws Exception
	 */
	public List<SObject> getObjectsSalesforce(QueryResult qr) throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {
		List<SObject> objects = new ArrayList<SObject>();
		logger.info("Total de registros encontrados: "  + qr.getSize());
		if (qr!=null){
			if (qr.getSize() > 0){				
				if (qr.getRecords()!=null) {
					for (int i=0;i<qr.getRecords().length;i++) {
						objects.add((SObject)qr.getRecords()[i]);
					}
				}												
			}
		}
		return objects;
	}

	public List<SObject> getObjectsByMapSalesforce(String strSOQLStmt,Map<String, Integer> mapForeignIds) throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {
		return this.getObjectsByMapSalesforce(strSOQLStmt,null,mapForeignIds);
	}	
	
	public List<SObject> getObjectsByMapSalesforce(String strSOQLStmt,Integer queryBatchSize,Map<String, Integer> mapForeignIds) throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {
		return this.getObjectsByMapSalesforce(strSOQLStmt,queryBatchSize,mapForeignIds,null);
	}
	
	public List<SObject> getObjectsByMapSalesforce(String strSOQLStmt,Integer queryBatchSize,Map<String, Integer> mapForeignIds,String orderBy) throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {		
		List<SObject> objects = new ArrayList<SObject>();
		
		if (mapForeignIds!=null) {
		
			Integer fromIndex = 0;
			Integer toIndex = 0;
			
			while(toIndex<mapForeignIds.size()) {
				Map<String,Integer> subMapForeignIds = new HashMap<String,Integer>();
				if (mapForeignIds.size()<=toIndex + MAXIMO_IDS_IN) {
					toIndex = mapForeignIds.size();												
				} else {
					toIndex += MAXIMO_IDS_IN;
				}	
				
				subMapForeignIds = UtilBasic.obtenerSubMapa(mapForeignIds, fromIndex,toIndex);
				
				String newStrSOQLStmt = strSOQLStmt + UtilBasic.obtenerLlaves(subMapForeignIds); 
				
				if (orderBy!=null && orderBy.length()>0) {
					newStrSOQLStmt += " order by " + orderBy;
				}				
				objects.addAll(this.getObjectsSalesforce(newStrSOQLStmt, queryBatchSize));
				fromIndex = toIndex;
			}
		}
		return objects;
	}	


	public List<SObject> getObjectsByMapDoubleSalesforce(String strSOQLStmt,Integer queryBatchSize,Map<String, Integer> mapForeignIds,String orderBy) throws RemoteException,LoginFault,InvalidSObjectFault,MalformedQueryFault,InvalidIdFault,InvalidFieldFault,UnexpectedErrorFault,InvalidQueryLocatorFault,Exception {		
		List<SObject> objects = new ArrayList<SObject>();
		
		if (mapForeignIds!=null) {
		
			Integer fromIndex = 0;
			Integer toIndex = 0;
			
			while(toIndex<mapForeignIds.size()) {
				Map<String,Integer> subMapForeignIds = new HashMap<String,Integer>();
				if (mapForeignIds.size()<=toIndex + MAXIMO_IDS_IN) {
					toIndex = mapForeignIds.size();												
				} else {
					toIndex += MAXIMO_IDS_IN;
				}	
				
				subMapForeignIds = UtilBasic.obtenerSubMapa(mapForeignIds, fromIndex,toIndex);
				
				String newStrSOQLStmt = strSOQLStmt + UtilBasic.obtenerLlavesDouble(subMapForeignIds); 
				
				if (orderBy!=null && orderBy.length()>0) {
					newStrSOQLStmt += " order by " + orderBy;
				}				
				objects.addAll(this.getObjectsSalesforce(newStrSOQLStmt, queryBatchSize));
				fromIndex = toIndex;
			}
		}
		return objects;
	}	
	
	
	
	public SforceServiceStub getBinding() throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		checkLogin();
		return this.binding;
	}
	
	/**
	* This method creates an array of sObjects with a specified
	* batchSize.
	* @param records
	* @param batchSize
	* @return SaveResult[]
	*/
	public SaveResult[] create(SObject[] records, int batchSize) throws InvalidSObjectFault, UnexpectedErrorFault, InvalidIdFault, RemoteException, Exception {
		if (batchSize > 200 || batchSize < 1) throw new IllegalArgumentException("batchSize must be between 1 and 200");
		return batch(records, batchSize, new CreateBatcher());
	}
	
	public SaveResult[] create(SObject[] records) throws InvalidSObjectFault, UnexpectedErrorFault, InvalidIdFault, RemoteException,Exception {
		return create(records, 200);
	}
	
	public SaveResult[] update(SObject[] records, int batchSize) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		if (batchSize > 200 || batchSize < 1) throw new IllegalArgumentException("batchSize must be between 1 and 200");
		return batch(records, batchSize, new UpdateBatcher());
	}
	
	
	public DeleteResult[] delete(ID[] records) throws InvalidSObjectFault, UnexpectedErrorFault, InvalidIdFault, RemoteException,Exception {
		return batch(records,200, new DeleteBatcher());
	}	
	
	
	/**
	* This method updates an array of sObjects with a specified
	* batchSize.
	* @param records
	* @param batchSize
	* @return SaveResult[]
	*/
	public SaveResult[] update(SObject[] records) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		return update(records, 200);
	}
	
	public SaveResult[] upsert(SObject[] records, int batchSize) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		if (batchSize > 200 || batchSize < 1) throw new IllegalArgumentException("batchSize must be between 1 and 200");
		return batch(records, batchSize, new UpsertBatcher());
	}
	
	/**
	* This method updates an array of sObjects with a specified
	* batchSize.
	* @param records
	* @param batchSize
	* @return SaveResult[]
	*/
	public SaveResult[] upsert(SObject[] records) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		return upsert(records, 200);
	}
	
	
	
	
	private SaveResult[] batch(SObject[] records, int batchSize, Batcher batchOperation) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		if (records.length <= batchSize) {
			checkLogin();
			return batchOperation.perform(records);
		}
		SaveResult[] saveResults = new SaveResult[records.length];
		SObject[] thisBatch = null;
		int pos = 0;
		while (pos < records.length) {			
			int thisBatchSize = Math.min(batchSize,	records.length - pos);
			if (thisBatch == null ||thisBatch.length != thisBatchSize) thisBatch = new SObject[thisBatchSize];
			logger.info("Records " + pos + "-" + (pos+thisBatchSize));
			
			System.arraycopy(records, pos, thisBatch, 0,thisBatchSize);
			SaveResult [] batchResults = batch(thisBatch,thisBatchSize,batchOperation);
			System.arraycopy(batchResults, 0, saveResults,pos, thisBatchSize);
			pos += thisBatchSize;
		}
		return saveResults;
	}
	
	
	private DeleteResult[] batch(ID[] records,int batchSize,DeleteBatcher batchOperation) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
		if (records.length <= batchSize) {
			checkLogin();
			return batchOperation.delete(records);
		}
		
		DeleteResult[] deleteResults = new DeleteResult[records.length];
		ID[] thisBatch = null;
		int pos = 0;
		while (pos < records.length) {			
			int thisBatchSize = Math.min(batchSize,	records.length - pos);
			if (thisBatch == null ||thisBatch.length != thisBatchSize) thisBatch = new ID[thisBatchSize];
			logger.info("Records " + pos + "-" + (pos+thisBatchSize));
			
			System.arraycopy(records, pos, thisBatch, 0,thisBatchSize);
			DeleteResult[] batchResults = batch(thisBatch,thisBatchSize,batchOperation);
			System.arraycopy(batchResults, 0, deleteResults,pos, thisBatchSize);
			pos += thisBatchSize;
		}
		return deleteResults;		
	}	
	
	
	private abstract class Batcher {
		abstract SaveResult[] perform(SObject [] records) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception;
	}
	
	private class CreateBatcher extends Batcher {
		SaveResult [] perform(SObject [] records) throws UnexpectedErrorFault, InvalidIdFault,LoginFault, RemoteException,Exception {
			checkLogin();
			return binding.create(records, sh, null, null, null, null, null, null);
		}		
	}
	
	private class UpdateBatcher extends Batcher {
		SaveResult [] perform(SObject [] records) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException, Exception {
			checkLogin();
			return binding.update(records, sh, null, null, null, null, null, null);
		}
	}
		
	private class UpsertBatcher extends Batcher {
		SaveResult [] perform(SObject [] records) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException, Exception {
			checkLogin();
			binding.update(records, sh, null, null, null, null, null, null);
			return null;
		}
	}
	
	private class DeleteBatcher {
		DeleteResult[] delete(ID[] records) throws UnexpectedErrorFault, InvalidIdFault, LoginFault, RemoteException,Exception {
			checkLogin();
			return binding.delete(records, sh, null, null, null, null, null);
		}
	}

	public ID getUserID() {
		return userID;
	}	
}
